#!/usr/bin/perl -w

# OVPN-CLI - A OpenVPN Command-Line-Interface
# Copyright (C) 2009-2010 Michael Weishaar (dev@datenschleuder.com)

# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

use strict;
use warnings;

use Switch;
use IO::File;
use Net::Telnet;
use File::Copy;

# IP/Port and password declaration for OVPN management interface

my $IP = "127.0.0.1";
my $PORT = 10000;
my $PASSWORD = "test";

# Environmental declarations

my $ovpn_dir = "/etc/openvpn";
my $rsa_dir  = "/etc/openvpn/rsa";
my $key_dir  = "/etc/openvpn/rsa/keys";

# Config for $MS/Linux Clients

my $CONFIG_WINDOWS = "
client
dev tun
proto udp
remote 172.19.0.3
redirect-gateway
resolv-retry infinite
nobind
persist-key
persist-tun
cipher AES-128-CBC 
comp-lzo
verb 3
";

# End of $MS config

my $match;
my $command_to_execute;
my $output;

CLI_MAIN();

sub CLI_MAIN
	{
	my $cli_main =
		"\n\n--------------------------------------------------\n\n"
		. "Welcome to the OpenVPN Command-Line-Interface (CLI)\n"
		. "Your options:\n\n"
		. "1. Generate certificates\n"
		. "2. Revoke certificates\n"
		. "3. Show connected clients\n"
		. "4. Block a client\n"
		. "5. Unblock a client\n"
		. "6. Show OVPN version\n"
		. "7. Exit CLI\n\n"
		. "Enter a number: ";

		print $cli_main;	

		system "stty", '-icanon', 'eol', "\001";
	
		my $answer = getc(STDIN);
		print "\n\n--------------------------------------------------\n\n";
		
	switch ($answer) 
		{

	case 1  { # Generate certificates
                GENKEYS();
		CLI_MAIN();
        	}

	case 2  { # Revoke certificates
		REVKEYS();
		CLI_MAIN();
		}

	case 3  { # Show connected clients
	    $command_to_execute = "status";
        CONNECT($command_to_execute);
        STATUS_MANIPULATE();
		CLI_MAIN();
        }

	case 4  { # Block a client
		BLOCK_CLIENT();	
		CLI_MAIN();
		}

	case 5  { # Unblock a client
		UNBLOCK_CLIENT();
		CLI_MAIN();
		}
	
	case 6  { # Show current OVPN version
        $command_to_execute = "version";
        CONNECT($command_to_execute);
        VERSION_MANIPULATE();
		CLI_MAIN(); 
        }

	case 7	{ # Exit
		print "\n\n";
		exit 0;
		}

	} # End switch statement

} # End CLI_MAIN


sub BLOCK_CLIENT
	{
	print "\nType in clients VPN-IP (or IP/CIDR for a range): ";
	chomp(my $ip_block = <STDIN>);
	system "iptables -I FORWARD -p ALL -s $ip_block -j REJECT";
	
	} # End BLOCK_CLIENT


sub UNBLOCK_CLIENT
	{
    print "\nType in clients VPN-IP (or IP/CIDR for a range): ";
    chomp(my $ip_unblock = <STDIN>);
    system "iptables -D FORWARD -p ALL -s $ip_unblock -j REJECT";
	
	} # End UNBLOCK_CLIENT


sub REVKEYS
	{
	my $rev_options =
		  "\nYour options:\n"
        	. "\n1. Single client"
        	. "\n2. Import CSV\n"
        	. "\nEnter a number: ";
		

	print $rev_options;

        system "stty", '-icanon', 'eol', "\001";

        if ( getc(STDIN) != 2 )
                {
                print "\nEnter a name for the certificate that should be revoked: ";
                
		chomp(my $cert_name = <STDIN>);
                
		system "cd $rsa_dir && source ./vars && $rsa_dir/revoke-full $cert_name";
                print "\nError 23 means, that the revoke was successful!\n";
		
		print "\nShould i delete the cert/key-files now? [y,n] ";              
		chomp(my $ask_delete = <STDIN>);
				
			if ($ask_delete eq "y")
				{
				system "rm -f $key_dir/$cert_name.*";
				}
          		
			CLI_MAIN();
                
		} # End if statement for option single client


        print "\n\nMake sure, that your copied the *.csv file to /etc/openvpn/ !\nFilename: ";
        chomp(my $csv_filename = <STDIN>);

        print "\n\nShould i also delete the cert/key-files? [y,n] ";
        chomp(my $ask_delete = <STDIN>);

        $csv_filename = $ovpn_dir . "/" . $csv_filename;

	my $fh = IO::File->new();
        $fh->open($csv_filename) or die "\n\nCannot open $csv_filename !\n\n";

        while (my $zeile = $fh->getline())
                {

                my @values = split(';', $zeile, 6);
                while ($values[2]=~s/\;//) {}
        	chomp(my $cert_name = $values[2] . $values[3] . $values[4] . $values[5]);

        	system "cd $rsa_dir && source ./vars && $rsa_dir/revoke-full $cert_name";

		print "\nError 23 means, that the revoke was successful!\n";

                if ($ask_delete eq "y")
 	               {
                	system "rm -f $key_dir/$cert_name.*";
	               }

		} # End zeilenimport

	$fh->close();
	
	} # End REVKEYS


sub GENKEYS
	{
        my $gen_options =
                  "\nYour options:\n"
                . "\n1. Single client"
                . "\n2. Import CSV\n"
                . "\nEnter a number: ";
		

        print $gen_options;

	system "stty", '-icanon', 'eol', "\001";

	if ( getc(STDIN) != 2 )
		{
		print "\nEnter a name for certificate: ";		
		chomp(my $cert_name = <STDIN>);

		print "How many days should cert be valid? ";
		chomp(my $cert_valid = <STDIN>);

		system "cd $rsa_dir && source ./vars && export KEY_EXPIRE=$cert_valid && $rsa_dir/build-key-pkcs12 $cert_name";

	        mkdir('/VPN',0600);
	        mkdir("/VPN/EINZELN",0600);
	        mkdir("/VPN/EINZELN/$cert_name",0600);

	        my $genfile = "$key_dir/$cert_name.p12";
	        my $newfile = "/VPN/EINZELN/$cert_name/";

	        copy($genfile, $newfile);  

	        open(config_temp, ">temp.ovpn");
	        print config_temp $CONFIG_WINDOWS;
	        close (config_temp);

	        open(config_temp2, ">>temp.ovpn");
	        print config_temp2 "pkcs12 $cert_name.p12";
	        close (config_temp2);

        	my $genconfig = "$ovpn_dir/temp.ovpn";
        	my $gendestination = "/VPN/EINZELN/$cert_name/windows.ovpn";

        	copy($genconfig, $gendestination);

        	unlink('temp.ovpn');

		CLI_MAIN();
		} # End if statement 


	print "\n\nMake sure, that your copied the *.csv file to /etc/openvpn/ !\nFilename: ";
	chomp(my $csv_filename = <STDIN>);
	print "\n";

	$csv_filename = $ovpn_dir . "/" . $csv_filename;
	
	my $fh = IO::File->new();
	$fh->open($csv_filename) or die "\n\nCannot open $csv_filename !\n\n";
	
	while (my $zeile = $fh->getline()) 
		{
		my @values = split(';', $zeile, 6);		
		while ($values[2]=~s/\;//) {}
	    	my $cert_name = $values[2] . $values[3] . $values[4] . $values[5];

		system "cd $rsa_dir && source ./vars && export KEY_EXPIRE=$values[1] && $rsa_dir/build-key-pkcs12 $cert_name";

		mkdir('/VPN',0600);	
		mkdir("/VPN/$values[0]",0600);
		mkdir("/VPN/$values[0]/$values[3]",0600);

		chomp($cert_name);
		chomp($values[0]);
		chomp($values[3]);

		my $genfile = "$key_dir/$cert_name.p12";
		my $newfile = "/VPN/$values[0]/$values[3]/";

		copy($genfile, $newfile); 				# 

        	open(config_temp, ">temp.ovpn");
        	print config_temp $CONFIG_WINDOWS;
        	close (config_temp);

        	open(config_temp2, ">>temp.ovpn");
        	print config_temp2 "pkcs12 $cert_name.p12";
        	close (config_temp2);

		my $genconfig = "$ovpn_dir/temp.ovpn";
		my $gendestination = "/VPN/$values[0]/$values[3]/windows.ovpn";        

		copy($genconfig, $gendestination);
	
		unlink('temp.ovpn');
        	
		} # End while

	$fh->close();
	
	} # End BUILDKEYS


sub CONNECT
        {
                my $out;
                my $conn = new Net::Telnet(
                
		Host => $IP,
                Port => $PORT);
                
		$conn->open();
                $conn->waitfor('/ENTER PASSWORD:/');
                $conn->print($PASSWORD);
                
		($out) = $conn->print($command_to_execute);
                ($match) = $conn->waitfor('/END/');
        } # END CONNECT


sub STATUS_MANIPULATE
        {
	substr $match, 0, 223, '';
        substr $match, -44, -1, '';
        while ($match=~s/ROUTING|TABLE|Virtual|Address|Common|Name|Real|Last|Ref//) {}

        # Search for ',' ' ' and \s+ (whitespaces) und delete them
        while ($match=~s/, | |\s+ //) {}


	my $base_output_path = '/etc/openvpn/logs'; 

	my $date = &get_formatted_date;

	my $output_path = $base_output_path . '/' . $date . '.txt';
	my $log_path = $base_output_path . '/' . $date . '.log';

	my %clients;
	my %clients_by_user;


	my @input;


	if(-r $log_path) {
	push @input, &read_file($log_path);
		}

	push @input, split /\r?\n/, $match;

	my $backup = new IO::File($log_path, '>');
	my %lines;

	for my $line (@input) {
	if(!exists $lines{$line}) {
		$backup->print($line, "\n");
		$lines{$line} = 1;
		}
	}
	$backup->close;


	for my $line (@input) {

		my @fields = split /,/, $line;
		my $client_complete;
		my $client_key;

	if(@fields == 5) {
		my $real_ip = &extract_ip($fields[1]);
		my $connected_since = &extract_time($fields[4]);

		my %client = (
			name            => $fields[0],
			real_ip         => $real_ip,
			incoming        => $fields[2],
			outgoing        => $fields[3],
			connected_since => $connected_since,
		);

		($client_complete, $client_key) = &merge_client(\%clients, \%client);
	}

	elsif(@fields == 4) {
		my $real_ip = &extract_ip($fields[2]);

		my %client = (
			vpn_ip  => $fields[0],
			name    => $fields[1],
			real_ip => $real_ip,
		);

		
		($client_complete, $client_key) = &merge_client(\%clients, \%client);
	}


	if($client_complete) {
		my $client = delete $clients{$client_key};

		my $name = $client->{name};
		my $connected_since = $client->{connected_since};
		my $traffic = &total_traffic($client);


		if(exists $clients_by_user{$name}->{connections}->{$connected_since}) {
			my $existing_client = $clients_by_user{$name}->{connections}->{$connected_since};

			if($traffic > $existing_client->{traffic}) {
				warn "$traffic > $existing_client->{traffic}\n";
				$existing_client->{traffic} = $traffic;
				$existing_client->{outgoing} = $client->{outgoing};
				$existing_client->{incoming} = $client->{incoming};
			}
		}
		else {
			$client->{traffic} = $traffic;
			$clients_by_user{$name}->{connections}->{$connected_since} = $client;
		}
	}
}



for my $user (values %clients_by_user) {
	my $traffic = 0;

	for my $connection (values %{$user->{connections}}) {
		$traffic += $connection->{traffic};
	}

	$user->{traffic} = $traffic;
}




# Sort clients by total traffic (descending).
my @clients = sort { $b->{traffic} <=> $a->{traffic} } values(%clients_by_user);


my $output_file = new IO::File($output_path, '>');


for my $user (@clients) {
	
	for my $client (values %{$user->{connections}}) {
		my $output =
			"Common Name: $client->{name}\t"
			. "Clients Real IP: $client->{real_ip}\t"
			. "Clients VPN IP: $client->{vpn_ip}\t"
			. "Connected since: $client->{connected_since}\t"
			. "Received Bytes: $client->{incoming}\t"
			. "Sent Bytes: $client->{outgoing}\t\n";

		print $output . "\n";
		$output_file->print($output);
	}
}

$output_file->close;


# Extract the IP from a IP:port string.
sub extract_ip {
	my ($host) = @_;

	$host =~ s/:.*$//;

	return $host;
}


sub merge_client {
	my ($clients, $client) = @_;

	my $real_ip = $client->{real_ip};
	my $client_complete = 0;

	my @keys = keys %$client;

	if(exists($clients->{$real_ip})) {
		@{$clients->{$real_ip}}{@keys} = @{$client}{@keys};
		$client_complete = 1;
	}
	else {
		$clients->{$real_ip} = $client;
	}

	return ($client_complete, $real_ip);
}

sub extract_time {
	my ($date) = @_;

	if($date =~ /(\d{2}:\d{2}:\d{2})/) {
		return $1;
	}

	return '<parse error>';
}



sub total_traffic {
	my ($client) = @_;

	return $client->{incoming} + $client->{outgoing};
}



sub get_formatted_date {
	my ($year, $month, $day) = (localtime)[5, 4, 3];
	my $date = sprintf('%02d_%02d_%04d', $day, $month + 1, $year + 1900);

	return $date;
}


sub read_file {
	my ($path) = @_;

	if(-r $path) {
		my @input;

		my $input = new IO::File($path);

		while(my $line = $input->getline) {
			chomp $line;
			push @input, $line;
		}

		$input->close;

		return @input;
	}

	else {
		warn "Warning! Can't read $path.\n";
		return ();
	}
}

} # END STATUS_MANIPULATE


sub VERSION_MANIPULATE
        {
                substr $match, 0, 120, '';
                substr $match, -23, -1, '';
		print "\n$match";
        } # END VERSION_MANIPULATE


exit 0;
